package org.study.minio.boot.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.*;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.study.minio.boot.config.MinioProperties;

import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Minio工具类
 *
 * @author doveylovey
 * @version v1.0.0
 * @email 1135782208@qq.com
 * @date 2023年10月07日
 */
@Slf4j
@UtilityClass
public class MinioUtils {
    // 允许的文件格式、大小(单位是MB)
    public final List<String> ALLOWED_IMG_TYPE = Lists.newArrayList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp");
    public final long ALLOWED_IMG_SIZE = 1L;
    public final List<String> ALLOWED_DOC_TYPE = Lists.newArrayList(".txt", ".md", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx");
    public final long ALLOWED_DOC_SIZE = 5L;
    public final List<String> ALLOWED_VIDEO_TYPE = Lists.newArrayList(".mp4", ".avi", ".mov", ".3gp");
    public final long ALLOWED_VIDEO_SIZE = 20L;
    public final List<String> ALLOWED_OTHER_TYPE = Lists.newArrayList(".zip", ".dwg", ".dxf");
    public final long ALLOWED_OTHER_SIZE = 10L;

    private MinioProperties minioProperties;
    private MinioClient minioClient;

    public void init(MinioProperties minioProperties, MinioClient client) {
        MinioUtils.minioProperties = minioProperties;
        MinioUtils.minioClient = client;
    }

    /**
     * 判断Bucket是否存在：true-存在、false-不存在
     *
     * @param bucketName 桶名
     */
    public boolean bucketExists(String bucketName) throws Exception {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建桶
     *
     * @param bucketName 桶名
     */
    public boolean createBucket(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            return true;
        }
        minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        return true;
    }

    /**
     * 设置痛的访问策略为public
     *
     * @param bucketName 桶名
     */
    public boolean setBucketPublicPolicy(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            String config = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build());
            return true;
        }
        return false;
    }

    /**
     * 设置痛的访问策略为private
     *
     * @param bucketName 桶名
     */
    public boolean setBucketPrivatePolicy(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            String config = "{\"Version\":\"2012-10-17\",\"Statement\":[]}";
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build());
            return true;
        }
        return false;
    }

    /**
     * 获取Bucket的策略
     *
     * @param bucketName 桶名
     */
    public String getBucketPolicy(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
        }
        return null;
    }

    /**
     * 获取所有Bucket列表
     */
    public List<Bucket> getAllBuckets() throws Exception {
        return minioClient.listBuckets();
    }

    /**
     * 根据bucketName获取其相关信息
     *
     * @param bucketName 桶名
     */
    public Bucket getBucket(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst().orElse(null);
        }
        return null;
    }

    /**
     * 根据bucketName删除Bucket：true-删除成功、false-删除失败
     *
     * @param bucketName 桶名
     */
    public boolean removeBucket(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
            return true;
        }
        return false;
    }

    /**
     * 强制删除一个桶：桶为空时直接删除，桶不为空时先删除桶中的数据，然后再删除桶
     *
     * @param bucketName 桶名
     */
    public boolean removeBucketForce(String bucketName) throws Exception {
        List<String> objectNameList = listObjectsName(bucketName, true);
        for (String objectName : objectNameList) {
            removeFile(bucketName, objectName);
        }
        return removeBucket(bucketName);
    }

    /**
     * 创建文件夹，必须以"/"结尾
     *
     * @param bucketName 桶名
     * @param folderName 文件夹名称
     * @return
     */
    public boolean createFolder(String bucketName, String folderName) throws Exception {
        ObjectWriteResponse response = minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(folderName)
                        .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
                        .build()
        );
        return true;
    }

    /**
     * 判断文件夹是否存在
     *
     * @param bucketName 桶名
     * @param folderName 文件夹名称
     */
    public boolean isFolderExist(String bucketName, String folderName) throws Exception {
        Iterable<Result<Item>> results = listObjects(bucketName, folderName, false);
        for (Result<Item> result : results) {
            Item item = result.get();
            if (item.isDir() && item.objectName().equals(folderName + "/")) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断是否为文件夹
     *
     * @param bucketName 桶名
     * @param folderName 文件夹名称
     */
    public boolean isFolder(String bucketName, String folderName) throws Exception {
        if (bucketExists(bucketName)) {
            Iterable<Result<Item>> iterable = listObjects(bucketName, folderName, false);
            // 不为空则说明是文件夹
            return iterable.iterator().hasNext();
        }
        return false;
    }

    /**
     * 删除文件夹，可以"/"结尾
     *
     * @param bucketName 桶名
     * @param folderName 文件夹名称
     * @return
     */
    public boolean deleteFolder(String bucketName, String folderName) throws Exception {
        List<String> objectsNameList = listObjectsName(bucketName, folderName, true);
        for (int i = 0; i < objectsNameList.size(); i++) {
            removeFile(bucketName, objectsNameList.get(i));
        }
        return true;
    }

    /**
     * 查看桶中的所有文件对象
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     */
    public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
        }
        return Collections.emptyList();
    }

    /**
     * 获取桶中所有文件路径
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     */
    public List<String> listObjectsName(String bucketName, boolean recursive) throws Exception {
        List<String> objectNameList = Lists.newArrayList();
        Iterable<Result<Item>> results = listObjects(bucketName, recursive);
        for (Result<Item> result : results) {
            String objectName = result.get().objectName();
            objectNameList.add(objectName);
        }
        return objectNameList;
    }

    /**
     * 根据文件名称前置查询文件
     *
     * @param bucketName 桶名
     * @param prefix     前缀
     * @param recursive  是否递归查询
     */
    public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) throws Exception {
        Iterable<Result<Item>> objectNameList = Lists.newArrayList();
        if (bucketExists(bucketName)) {
            objectNameList = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(bucketName)
                            .prefix(prefix)
                            .recursive(recursive)
                            .build()
            );
        }
        return objectNameList;
    }

    /**
     * 获取文件夹中的所有文件路径
     *
     * @param bucketName 桶名
     * @param folderName 文件夹名
     * @param recursive  是否递归查询
     */
    public List<String> listObjectsName(String bucketName, String folderName, boolean recursive) throws Exception {
        List<String> objectNameList = Lists.newArrayList();
        Iterable<Result<Item>> results = listObjects(bucketName, folderName, recursive);
        for (Result<Item> item : results) {
            objectNameList.add(item.get().objectName());
        }
        return objectNameList;
    }

    /**
     * 获取文件流
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     */
    public InputStream getObject(String bucketName, String objectName) throws Exception {
        GetObjectResponse response = minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()
        );
        return response;
    }

    /**
     * 获取文件字节数组
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     */
    public byte[] getObjectByte(String bucketName, String objectName) throws Exception {
        try (InputStream inputStream = getObject(bucketName, objectName);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            int len = -1;
            byte[] buffer = new byte[1024];
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            return outputStream.toByteArray();
        }
    }

    /**
     * 断点下载，此操作需要对此Object具有读权限
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param offset     起始字节的位置
     * @param length     要读取的长度
     */
    public InputStream getObject(String bucketName, String objectName, long offset, long length) throws Exception {
        GetObjectResponse response = minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build()
        );
        return response;
    }

    /**
     * 获取文件外链
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     */
    public String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .build()
            );
        }
        return null;
    }

    /**
     * 获取文件外链
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param expires    外链有效时间，单位：秒，有效值范围：1<= expires <= 7*24*60*60(7天)
     */
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .expiry(expires)
                            .build()
            );
        }
        return null;
    }

    /**
     * 获取文件外链
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param expires    外链有效时间，有效值范围：1秒 <= expires <= 7天
     * @param unit       外链有效时间单位
     */
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires, TimeUnit unit) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectName)
                            .expiry(expires, unit)
                            .build()
            );
        }
        return null;
    }

    /**
     * 通过文件在MinIO中的访问路径获取对象名称
     *
     * @param bucketName 桶名
     * @param url        文件在MinIO中的访问路径
     */
    public String getObjectName(String bucketName, String url) {
        if (StringUtils.isNotBlank(url)) {
            String baseUrl = minioProperties.getEndpoint() + "/" + bucketName;
            return url.substring(url.indexOf(baseUrl) + baseUrl.length() + 1);
        }
        return null;
    }

    /**
     * 通过文件路径获取文件名称
     *
     * @param filepath 文件路径
     */
    public String getFileName(String filepath) {
        if (StringUtils.isNotBlank(filepath)) {
            return filepath.substring(filepath.lastIndexOf("/") + 1);
        }
        return null;
    }

    /**
     * 获取文件信息，如果抛出异常则说明文件不存在
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @return
     */
    public StatObjectResponse getObjectStat(String bucketName, String objectName) throws Exception {
        if (bucketExists(bucketName)) {
            return minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build()
            );
        }
        return null;
    }

    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName  桶名
     * @param file        文件
     * @param objectName  对象名
     * @param contentType 类型，可使用MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();获取
     */
    public String uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) throws Exception {
        try (InputStream inputStream = file.getInputStream()) {
            ObjectWriteResponse objectWriteResponse = minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .contentType(contentType)
                            .stream(inputStream, file.getSize(), -1)
                            //.stream(inputStream, inputStream.available(), -1)
                            .build()
            );
            /*if (StringUtils.isNotBlank(objectWriteResponse.etag())) {
                return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
            }*/
            return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
        }
    }

    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName 桶名
     * @param file       文件
     * @param objectName 对象名
     */
    public String uploadFile(String bucketName, MultipartFile file, String objectName) throws Exception {
        return uploadFile(bucketName, file, objectName, file.getContentType());
    }

    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName 桶名
     * @param file       文件
     */
    public String uploadFile(String bucketName, MultipartFile file) throws Exception {
        String format = DateTimeFormatter.ofPattern(DatePattern.NORM_MONTH_PATTERN).format(LocalDate.now());
        String filename = file.getOriginalFilename();
        String objectName = format + "/" + filename;
        return uploadFile(bucketName, file, objectName);
    }

    /**
     * 上传本地文件
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param filepath   本地文件路径
     * @return
     */
    public String uploadFile(String bucketName, String objectName, String filepath) throws Exception {
        ObjectWriteResponse objectWriteResponse = minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(filepath)
                        .build()
        );
        /*if (StringUtils.isNotBlank(objectWriteResponse.etag())) {
            return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
        }*/
        return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
    }

    /**
     * 上传本地文件
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param file       本地文件
     * @return
     */
    public String uploadFile(String bucketName, String objectName, File file) throws Exception {
        return uploadFile(bucketName, objectName, file.getAbsolutePath());
    }

    /**
     * 上传文件
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param data       字节数组
     * @return
     */
    public String uploadFile(String bucketName, String objectName, byte[] data) throws Exception {
        return uploadFile(bucketName, objectName, new ByteArrayInputStream(data));
    }

    /**
     * 上传本地文件
     *
     * @param bucketName 桶名
     * @param file       本地文件
     * @return
     */
    public String uploadFile(String bucketName, File file) throws Exception {
        String format = DateTimeFormatter.ofPattern(DatePattern.NORM_MONTH_PATTERN).format(LocalDate.now());
        String filename = file.getName();
        String objectName = format + "/" + filename;
        return uploadFile(bucketName, objectName, file.getAbsolutePath());
    }

    /**
     * 通过流上传文件
     *
     * @param bucketName  桶名
     * @param objectName  文件名
     * @param inputStream 文件流
     * @return
     */
    public String uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {
        ObjectWriteResponse objectWriteResponse = minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .build()
        );
        /*if (StringUtils.isNotBlank(objectWriteResponse.etag())) {
            return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
        }*/
        return minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
    }

    /**
     * 图片上传
     *
     * @param bucketName 桶名
     * @param imgBase64  图片的base64字符串
     * @param imageName  图片名，带后缀
     */
    public String uploadImage(String bucketName, String imgBase64, String imageName) throws Exception {
        byte[] bytes = Base64.decode(imgBase64.trim());
        try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
            String dir = DateTimeFormatter.ofPattern(DatePattern.NORM_MONTH_PATTERN).format(LocalDate.now());
            return uploadFile(bucketName, dir + "/" + imageName, inputStream);
        }
    }

    /**
     * 图片上传
     *
     * @param bucketName 桶名
     * @param imgBase64  图片的base64字符串
     */
    public String uploadImage(String bucketName, String imgBase64) throws Exception {
        String imageName = IdUtil.getSnowflakeNextIdStr() + ".png";
        return uploadImage(bucketName, imgBase64, imageName);
    }

    /**
     * 合并指定桶中某个文件夹下的所有文件，将分块文件组成一个新的文件
     *
     * @param srcBucketName 分块文件所在的桶
     * @param folderName    文件夹名称
     * @param desBucketName 合并文件生成文件所在的桶
     * @param desObjectName 存储于桶中的对象名
     */
    public String composeObject(String srcBucketName, String folderName, String desBucketName, String desObjectName) throws Exception {
        List<String> objectNameList = listObjectsName(srcBucketName, folderName, true);
        if (ObjectUtils.isEmpty(objectNameList)) {
            throw new RuntimeException(srcBucketName + "/" + folderName + "文件夹中没有文件可以合并");
        }
        List<ComposeSource> composeSourceList = Lists.newArrayList();
        objectNameList = objectNameList.stream().sorted().collect(Collectors.toList());
        for (String object : objectNameList) {
            composeSourceList.add(ComposeSource.builder().bucket(srcBucketName).object(object).build());
        }
        return composeObject(composeSourceList, desBucketName, desObjectName);
    }

    /**
     * 合并指定桶中的所有文件，将分块文件组成一个新的文件
     *
     * @param srcBucketName 分块文件所在的桶
     * @param desBucketName 合并文件生成文件所在的桶
     * @param desObjectName 存储于桶中的对象名
     */
    public String composeObject(String srcBucketName, String desBucketName, String desObjectName) throws Exception {
        List<String> objectNameList = listObjectsName(srcBucketName, false);
        if (ObjectUtils.isEmpty(objectNameList)) {
            throw new RuntimeException(srcBucketName + "桶中没有文件可以合并");
        }
        List<ComposeSource> composeSourceList = Lists.newArrayList();
        objectNameList = objectNameList.stream().sorted().collect(Collectors.toList());
        for (String object : objectNameList) {
            composeSourceList.add(ComposeSource.builder().bucket(srcBucketName).object(object).build());
        }
        return composeObject(composeSourceList, desBucketName, desObjectName);
    }

    /**
     * 文件合并，将分块文件组成一个新的文件
     *
     * @param bucketName       合并文件生成文件所在的桶
     * @param objectName       原始文件名
     * @param sourceObjectList 分块文件集合
     */
    public String composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) throws Exception {
        minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .sources(sourceObjectList)
                        .build()
        );
        // String url = getPresignedObjectUrl(bucketName, objectName);
        String url = minioProperties.getEndpoint() + "/" + bucketName + "/" + objectName;
        return url;
    }

    /**
     * 拷贝文件
     *
     * @param srcBucketName 桶名
     * @param srcObjectName 文件名
     * @param desBucketName 目标存储桶
     * @param desObjectName 目标文件名
     */
    public ObjectWriteResponse copyFile(String srcBucketName, String srcObjectName, String desBucketName, String desObjectName) throws Exception {
        if (bucketExists(srcBucketName)) {
            return minioClient.copyObject(
                    CopyObjectArgs.builder()
                            .source(CopySource.builder().bucket(srcBucketName).object(srcObjectName).build())
                            .bucket(desBucketName)
                            .object(desObjectName)
                            .build()
            );
        }
        log.error("待拷贝的文件不存在");
        return null;
    }

    /**
     * 下载文件到本地，默认存储到项目根目录。可指定filename路径：D:\\file\\test.txt
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param localPath  本地路径
     */
    public void download(String bucketName, String objectName, String localPath) throws Exception {
        String decode = URLDecoder.decode(localPath, StandardCharsets.UTF_8);
        String filename = decode.substring(decode.lastIndexOf("/") + 1);
        StatObjectResponse statObject = getObjectStat(bucketName, objectName);
        if (statObject != null && statObject.size() > 0) {
            minioClient.downloadObject(
                    DownloadObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .filename(filename)
                            .build()
            );
        }
    }

    /**
     * 文件下载
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     * @param response
     */
    public void download(String bucketName, String objectName, HttpServletResponse response) throws Exception {
        String decode = URLDecoder.decode(objectName, StandardCharsets.UTF_8);
        String filename = decode.substring(decode.lastIndexOf("/") + 1);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        // 设置强制下载不打开
        response.setContentType("application/force-download");
        response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
        // 获取对象信息
        StatObjectResponse statObject = getObjectStat(bucketName, objectName);
        // 以流的形式获取一个文件对象
        if (statObject != null && statObject.size() > 0) {
            try (InputStream input = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build())) {
                byte[] buffer = new byte[1024];
                try (OutputStream outputStream = response.getOutputStream()) {
                    int read = 0;
                    while ((read = input.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, read);
                    }
                    outputStream.flush();
                }
            }
        }
    }

    /**
     * 通过文件名称下载文件
     *
     * @param bucketName 桶名
     * @param objectName 对象存储名称
     * @param objectName 文件名
     */
    public void download(String bucketName, String objectName, String filename, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try (GetObjectResponse getResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build())) {
            int len = 0;
            byte[] buf = new byte[1024];
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
                while ((len = getResponse.read(buf)) != -1) {
                    os.write(buf, 0, len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                // 设置强制下载不打开
                response.setContentType("application/force-download");
                response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
                // filenameEncoding 方法兼容不同浏览器编码
                response.addHeader("Content-Disposition", "attachment;filename=" + filenameEncoding(filename, request));
                try (ServletOutputStream stream = response.getOutputStream()) {
                    stream.write(bytes);
                    stream.flush();
                }
            }
        }
    }

    /**
     * 批量下载
     *
     * @param bucketName   桶名
     * @param filenameList 多个文件名称
     * @param zipName      压缩包名称，不带后缀.zip
     */
    public void downloadBatch(String bucketName, List<String> filenameList, String zipName, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.reset();
        try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
             ZipOutputStream out = new ZipOutputStream(bos)) {
            response.setHeader("Access-Control-Allow-Origin", "*");
            for (int i = 0; i < filenameList.size(); i++) {
                InputStream object = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(filenameList.get(i)).build());
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                // 设置强制下载不打开
                response.setContentType("application/force-download");
                response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
                response.setHeader("Content-Disposition", "attachment;filename=" + filenameEncoding(zipName, request) + ".zip");
                out.putNextEntry(new ZipEntry(filenameList.get(i)));
                int length = 0;
                byte buf[] = new byte[1024];
                while ((length = object.read(buf)) > 0) {
                    out.write(buf, 0, length);
                }
            }
        }
    }

    /**
     * 设置不同浏览器编码
     *
     * @param filename 文件名称
     * @param request  请求对象
     */
    public String filenameEncoding(String filename, HttpServletRequest request) {
        // 获得请求头中的User-Agent
        String agent = request.getHeader("User-Agent");
        // 根据不同的客户端进行不同的编码
        if (agent.contains("MSIE")) {
            // IE浏览器
            filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
        } else if (agent.contains("Firefox")) {
            // 火狐浏览器
            filename = "=?utf-8?B?" + java.util.Base64.getEncoder().encode(filename.getBytes(StandardCharsets.UTF_8)) + "?=";
        } else {
            // 其它浏览器
            filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
        }
        return filename;
    }

    /**
     * 删除文件
     *
     * @param bucketName 桶名
     * @param objectName 文件名
     */
    public boolean removeFile(String bucketName, String objectName) throws Exception {
        if (bucketExists(bucketName)) {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
            return true;
        }
        return false;
    }

    /**
     * 根据文件全路径删除文件
     *
     * @param bucketName 桶名
     * @param url        文件在MinIO中的访问路径
     */
    public boolean removeFileByUrl(String bucketName, String url) throws Exception {
        if (bucketExists(bucketName)) {
            String objectName = getObjectName(bucketName, url);
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
            return true;
        }
        return false;
    }

    /**
     * 批量删除文件
     *
     * @param bucketName 桶名
     * @param keys       需要删除的文件列表
     */
    public Iterable<Result<DeleteError>> removeFile(String bucketName, List<String> keys) throws Exception {
        List<DeleteObject> objects = Lists.newLinkedList();
        keys.forEach(key -> objects.add(new DeleteObject(key)));
        if (bucketExists(bucketName)) {
            // 懒惰地删除多个对象，需要迭代返回的Iterable才能执行删除
            Iterable<Result<DeleteError>> response = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
            for (Result<DeleteError> result : response) {
                DeleteError error = result.get();
                log.error("[Minio工具类]批量删除文件异常。objectName=" + error.objectName() + "，errorMsg=" + error.message());
            }
            return response;
        }
        return Collections.emptySet();
    }

    /**
     * 根据文件全路径批量删除文件
     *
     * @param bucketName 桶名
     * @param urlList    文件在MinIO中的访问路径列表
     */
    public Iterable<Result<DeleteError>> removeFileByUrl(String bucketName, List<String> urlList) throws Exception {
        List<DeleteObject> objects = Lists.newLinkedList();
        urlList.forEach(key -> objects.add(new DeleteObject(getObjectName(bucketName, key))));
        if (bucketExists(bucketName)) {
            // 懒惰地删除多个对象，需要迭代返回的Iterable才能执行删除
            Iterable<Result<DeleteError>> response = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
            for (Result<DeleteError> result : response) {
                DeleteError error = result.get();
                log.error("[Minio工具类]批量删除文件异常。objectName=" + error.objectName() + "，errorMsg=" + error.message());
            }
            return response;
        }
        return Collections.emptySet();
    }

    /**
     * 获取文件后缀名，如.jpg、.pdf
     */
    public String getMultipartFileSuffix(MultipartFile file) {
        String filename = file.getOriginalFilename();
        String suffix = filename.substring(filename.lastIndexOf("."));
        return suffix;
    }

    /**
     * 获取文件后缀名，如.jpg、.pdf
     */
    public String getFileSuffix(File file) {
        String filename = file.getName();
        String suffix = filename.substring(filename.lastIndexOf("."));
        return suffix;
    }

    /**
     * 校验文件格式是否为指定格式
     *
     * @param suffix     文件后缀名，如.jpg、.pdf
     * @param allowTypes 允许的文件类型列表
     */
    public boolean isFileTypeAllowed(String suffix, List<String> allowTypes) {
        boolean isFileTypeAllowed = false;
        if (CollectionUtils.isNotEmpty(allowTypes) && StringUtils.isNotBlank(suffix)) {
            isFileTypeAllowed = allowTypes.contains(suffix.toLowerCase());
        }
        log.info("是否允许：{}，当前文件后缀名：{}，系统允许的文件类型：{}", isFileTypeAllowed, suffix, JSON.toJSONString(allowTypes));
        return isFileTypeAllowed;
    }

    /**
     * 判断文件是否超过指定大小
     *
     * @param fileSize     文件大小，单位是字节
     * @param expectedSize 文件大小上限，单位是MB
     */
    public boolean isFileSizeOver(long fileSize, long expectedSize) {
        long expectedBytes = expectedSize << 20;
        boolean isFileSizeOver = fileSize >= expectedBytes;
        log.info("是否超限：{}，当前文件大小：{}，系统配置的文件大小阈值：{}", isFileSizeOver, fileSize, expectedBytes);
        return isFileSizeOver;
    }

    /**
     * 文件后缀校验
     *
     * @param suffix      文件实际后缀
     * @param allowSuffix 允许的文件后缀
     * @return
     */
    private boolean isFileSuffixAllowed(String suffix, List<String> allowSuffix) {
        List<String> lowerCaseList = allowSuffix.stream()
                .map(item -> item.toLowerCase())
                .collect(Collectors.toList());
        return lowerCaseList.contains(suffix.toLowerCase());
    }

    /**
     * 文件大小、后缀校验
     *
     * @param fileSize 文件实际大小，单位是字节
     * @param suffix   文件实际后缀
     * @return
     */
    public void checkFileSizeAndSuffix(long fileSize, String suffix) {
        if (isFileSuffixAllowed(suffix, ALLOWED_IMG_TYPE)) {
            isFileSizeOver(fileSize, ALLOWED_IMG_SIZE);
        } else if (isFileSuffixAllowed(suffix, ALLOWED_DOC_TYPE)) {
            isFileSizeOver(fileSize, ALLOWED_DOC_SIZE);
        } else if (isFileSuffixAllowed(suffix, ALLOWED_VIDEO_TYPE)) {
            isFileSizeOver(fileSize, ALLOWED_VIDEO_SIZE);
        } else if (isFileSuffixAllowed(suffix, ALLOWED_OTHER_TYPE)) {
            isFileSizeOver(fileSize, ALLOWED_OTHER_SIZE);
        } else {
            throw new IllegalArgumentException("不允许上传" + suffix + "类型的文件");
        }
    }
}